# encoding: utf-8

class String
  # call-seq:
  #  String.escape_char  ==>  String or TypeError
  #
  # LaTex characters '$ _ % & # ~ \' will be escaped with '\'
  #
  # ==Return:
  # [String] _str_ in which all Latex chars were replaced
  # ==Example
  # 'A & B or C_'.escape_char('A & B or C_')  ==> 'A \& B or C\_'
  def escape_tex_char
    # Raise an TypeError-exception if *str* not of type _String_
    raise(TypeError, "Can't convert '#{self.class.name}' into String") unless self.respond_to?(:to_str)

    # Define an array including all chars which should be escaped
    chars = %w[$ _ % & # ~ \\]

    # Escape all chars in *self* which are equal to *chars*
    return self.gsub(Regexp.union(*chars)) { "\\" + $& }
  end
end

class Array  
  # call-seq:
  #  Array.include_error(<i>method</i>, </i>e</i>, <i>*args</i>)
  #
  # Method save occurring errors into _self_
  #
  # ==Arguments:
  # [method <i>(__method__] The object __method__
  # [e <i>(Exception)</i>]  Exception object
  # [*args <i>Array</i>]  Arguments passed to __method__
  # ==Example:
  #  Array::include_error( __method__, e , ['a','b'] )
  def include_error(method, e, *args)
    self << ["Method   : " + method.to_s,
             "Parameter: " + args.to_s,
             "Message  : " + e.message,
             "Backtrace: " + e.backtrace.reverse.join("\n")]
  end
end

class Table

  attr_accessor :errors,
                :data,
                :header,
                :caption,
                :caption_toc,
                :label
                :column_types

  # call-seq:
  #  Table.new(<i>args</i>) -> Table-Object
  #
  #  ==Arguments
  #  The initialization of the class Table requires a Hash including the following keys:
  # [:caption <i>(String)</i>] Caption of the table occurring above/below the table.
  # [:caption_toc <i>(String)</i>] Caption occurring in the list of tables (LOT)
  # [:col_def <i>(Array)</i>] The definition of the column types (e.g.: ['c', 'c', 'c']).
  # [:header <i>(Array)</i>] Header for the table
  # [:data <i>Array)</i>]  Data sets of the table
  # ==Return
  # [Table-Object]
  # ==Example
  def initialize(args)
    @errors = []

    @data = args[:data]
    
  end

  # call-seq:
  #  Table.longtable(Hash)  ==> String or ArgumentError
  #
  # Method returns a string containing the LaTeX code for a longtable.
  # The method requires a Hash including the following keys:
  # [:caption <i>(String)</i>] Caption of the table occurring above/below the table.
  # [:caption_toc <i>(String)</i>] Caption occurring in the list of tables (LOT)
  # [:col_def <i>(Array)</i>] The definition of the column types (e.g.: ['c', 'c', 'c']).
  # [:header <i>(Array)</i>] Header for the table
  # [:data <i>(Array) or (Arrays in Array)</i>]  Data sets of the table as
  #
  # == Example
  #  tbl = Table.new()
  #  puts tbl.longtable(
  #    caption: "This text will appear above the table",
  #    caption_toc: "This text will appear in the table of contents",
  #    label = "Tab001",
  #    col_def: ['c', 'c', 'c'],
  #    header: ['Column 1', 'A & B', 'Column 3'],
  #    data: [[1,2,3], [4,'A & B',5], [6,7,8]]
  #   )
  def longtable(args)

    # Check if arguments are correct
    raise(ArgumentError, "Number of columns in ':col_def' not equal with number of columns in ':header'!") unless args[:col_def].count == args[:header].count

    # Generate the longtable LaTex code
    tbl = "\\begingroup"
    tbl << "\\centering\n"

    # Add caption
    if args.include?(:caption_toc) and not args[:caption_toc].empty?
      tbl << "\\captionof{table}[#{args[:caption_toc]}]{#{args[:caption]}}\n"
    else
      tbl << "\\captionof{table}{#{args[:caption]}}\n"
    end

    tbl << "\\label{#{args[:label]}}\n" # Insert label
    tbl << "\\begin{longtable}{#{args[:col_def].join(' ')}}\n" # Begin longtable
    tbl << "\\toprule\n" # Toprule (Latex Package 'bookmarks')
    tbl << table_row(args[:header]) # Insert longtable header rows
    tbl << "\\midrule\n" # Midrule (Latex Package 'bookmarks')
    tbl << table_row(args[:data]) # Insert longtable data rows
    tbl << "\\bottomrule\n" # Bottomrule (Latex Package 'bookmarks')
    tbl << "\\end{longtable}\n" # End Longtable
    tbl << "\\endgroup\n\n"

    return tbl
  rescue ArgumentError => e
    @errors.include_error(__method__, e, args)
    return ArgumentError.name
  end

  # call-seq:
  #   Table.table_row(<i>tnl='\tabularnewline'</i>)  ==>  String or ArgumentError
  # Method returns a Latex table rows
  # ==Arguments:
  # [tnl <i>(String)</i>] Latex table row separator (<i>Default:</i> \tabularnewline)
  # ==Return
  # [String] A Latex table row (e.g.: A & B & C\\tabularnewline)
  # [ArgumentError] Name of the ArgumentError if an error occurs
  # ==Example
  #  Table.table_row( ['a','b','c'] )                  ==> "a & b & c\\tabularnewline\n"
  #  Table.table_row( [['a','b','c'], ['d','e','f']] ) ==> "a & b & c\\tabularnewline\n
  #                                                       d & e & f\\tabularnewline"
  #  Table.table_row( ['a','b','c'], '\\' )            ==> "a & b & c\\\\\n"
  def table_row(tnl='\tabularnewline')

    # Repeat method if data contains an array
    return @data.map { |d| table_row(d) }.join('') if @data.all? { |d| d.kind_of?(Array) }

    # Return a valid table row in LaTeX
    return @data.map! { |i| i.to_s.strip.escape_tex_char unless i.nil? }.join(' & ').gsub(/&\s+&/, '& ~ &') + tabularnewline + "\n"
  rescue ArgumentError => e
    @errors.include_error(__method__, e, data)
    return ArgumentError.name
  end

end
